package com.informatics.polymer.server.Polymerisation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.ParsingException;
import nu.xom.ValidityException;

import org.xmlcml.cml.element.CMLAtom;
import org.xmlcml.cml.element.CMLBond;
import org.xmlcml.cml.element.CMLFragment;
import org.xmlcml.cml.element.CMLJoin;
import org.xmlcml.cml.element.CMLMolecule;

/**
 * Class to build dendrimers.<br>
 * </br> <b>Example</b><br>
 * </br> Dendrimerisation dendrimerisation = new
 * Dendrimerisation(2,"c1(C[R])cc(C[R])cc(C[R])c1");<br>
 * </br> String dendrimer2D = dendrimerisation.getPolymer().toXML();<br>
 * </br> <b>Note</b><br>
 * </br> 3D coordinate generation at present is not available.
 * 
 * @author Ed Cannon
 * @version 1.0
 */
public class Dendrimerisation implements Polymerisation {
	/**
	 * Integer value representing the number of layers to build the dendrimer.
	 */
	private int layers;
	/**
	 * CMLMolecule representing the repeat units to be used in the
	 * dendrimerisation.
	 */
	private CMLMolecule repeatUnitCentre;
	/**
	 * CMLMolecule representation of the built dendrimer.
	 */
	private CMLMolecule dendrimer;
	/**
	 * . Maximum number of layers
	 */
	private static final int LAYER_MAXIMUM = 3;

	/**
	 * Constructs an object to undertake dendrimerisation.
	 */
	public Dendrimerisation() {
	}

	/**
	 * Constructs an object to undertake dendrimerisation, using two parameters,
	 * an integer value for the number of layers, and a String representation of
	 * the repeat unit to be dendrimerised. Currently restricted to upto 3
	 * layers.
	 * 
	 * @param numberOfLayers
	 *            - int value representing the number of layers to be built
	 *            around the repeat unit centre. Repeat units are attached to
	 *            all R-group points of the repeat unit centre for each layer
	 *            the process is repeated for additional layers.
	 * @param repeatUnit
	 *            -SMILE string representation of the repeat unit used in the
	 *            dendrimerisation.
	 * 
	 */
	public Dendrimerisation(final int numberOfLayers, final String repeatUnit) {
		if (numberOfLayers < 1 || numberOfLayers > LAYER_MAXIMUM) {
			throw new IllegalArgumentException("Only 1-3 layers are supported");
		} else {
			this.layers = numberOfLayers;
		}
		if (repeatUnit == null || repeatUnit == "") {
			throw new NullPointerException(
					"No repeat unit entered, please enter a repeat unit smile string");
		} else {
			int rCount = PolymerisationUtils.countNumberofFragmentPoints(
					repeatUnit, "[R]");

			if (rCount < 1) {
				throw new IllegalArgumentException(
						"Repeat units must have one or more R-groups!");
			} else {
				this.repeatUnitCentre = PolymerisationUtils
						.convertSmileString2CML(repeatUnit, "RU");
			}
		}
	}

	/**
	 * Constructs an object to undertake dendrimerisation, using two parameters,
	 * an integer value for the number of layers, and a CMLMolecule
	 * representation of the repeat unit to be dendrimerised. Currently
	 * restricted to upto 3 layers.
	 * 
	 * @param numberOfLayers
	 *            - int value representing the number of layers to be built
	 *            around the repeat unit centre. Repeat units are attached to
	 *            all R-group points of the repeat unit centre for each layer
	 *            the process is repeated for additional layers.
	 * @param repeatUnit
	 *            -CMLMolecule representation of the repeat unit used in the
	 *            dendrimerisation.
	 * 
	 */
	public Dendrimerisation(final int numberOfLayers,
			final CMLMolecule repeatUnit) {
		if (numberOfLayers < 1 || numberOfLayers > LAYER_MAXIMUM) {
			throw new IllegalArgumentException("Only 1-3 layers are supported");
		} else {
			this.layers = numberOfLayers;
		}

		if (repeatUnit == null) {
			throw new NullPointerException(
					"No repeat unit entered, please enter a repeat unit");
		} else {
			if (PolymerisationUtils.getRElements(repeatUnit).size() < 1) {
				throw new IllegalArgumentException(
						"Repeat unit needs 1 R group!");
			} else {
				this.repeatUnitCentre = PolymerisationUtils.addId(repeatUnit,
						"");
			}
		}

		this.repeatUnitCentre = repeatUnit;
	}

	/**
	 * Sets the number of layers.
	 * 
	 * @param layerValue
	 *            - an int value for the number of layers
	 */
	public final void setLayers(final int layerValue) {
		layers = layerValue;
	}

	/**
	 * Sets the CMLMolecule for the repeat unit.
	 * 
	 * @param repeatUnitValue
	 *            - CMLMolecule to represent the repeat unit centre in
	 *            dendrimerisation
	 */
	public final void setRepeatUnit(final CMLMolecule repeatUnitValue) {
		repeatUnitCentre = repeatUnitValue;
	}

	/**
	 * Return int value representing the number of layers to be built around the
	 * repeat unit centre. Repeat units are attached to all R-group points of
	 * the repeat unit centre for each layer.
	 * 
	 * @return layers
	 */
	public final int getLayers() {
		return layers;
	}

	/**
	 * Returns CMLMolecule representation of the repeat unit used in the
	 * dendrimerisation.
	 * 
	 * @return repeatUnitCentre
	 */
	public final CMLMolecule getRepeatUnit() {
		return repeatUnitCentre;
	}

	/**
	 * Returns XML String representation of the built polymer in 2D.
	 * 
	 * @return dendrimer.toXML() (non-Javadoc)
	 * @see org.xmlcml.cml.tools.polymer.Polymerisation#generatePML()
	 */
	public final String generatePML() {
		return dendrimer.toXML();
	}

	/**
	 * Generates a list of CMLMolecules, given the number of required layers
	 * (between 1 and 3), the CMLMolecule representing the repeat unit and a
	 * string id for the repeat unit, necessary to avoid molecule duplication.
	 * 
	 * @param numberOfRepeatUnits
	 *            - the total number of individual repeat units making up the
	 *            dendrimer
	 * @param ru
	 *            - CMLMolecule representation of the repeat unit
	 * @param extraId
	 *            -unique id added to repeat unit
	 * @return ArrayList<CMLMolecule>
	 */
	private ArrayList<CMLMolecule> countExpressionExpander(
			final int numberOfRepeatUnits, final CMLMolecule ru,
			final String extraId) {
		ArrayList<CMLMolecule> moleculesInRepeatUnit = new ArrayList<CMLMolecule>();
		for (int i = 0; i < numberOfRepeatUnits; i++) {
			CMLMolecule repeatUnitWithCountExpression = new CMLMolecule();
			PolymerisationUtils.addingAtomsAndBonds(ru, i, extraId,
					repeatUnitWithCountExpression);
			/*
			 * for(int j=0;j<ru.getAtomCount();j++){ CMLAtom a = new CMLAtom();
			 * a.setAttribute("id",
			 * extraId+Integer.toString(i)+ru.getAtom(j).getId());
			 * a.setAttribute("elementType", ru.getAtom(j).getElementType());
			 * a.setFormalCharge(ru.getAtom(j).getFormalCharge()); //Add
			 * aromaticity flag
			 * if(ru.getAtom(j).getAttribute("aromatic")!=null){
			 * a.setAttribute("aromatic",
			 * ru.getAtom(j).getAttributeValue("aromatic")); }
			 * repeatUnitWithCountExpression.addAtom(a); } for(int
			 * j=0;j<ru.getBondCount();j++){ CMLBond b = new CMLBond(); String
			 * id1 = ru.getBonds().get(j).getAtomId(0); String id2 =
			 * ru.getBonds().get(j).getAtomId(1);
			 * b.setAttribute("atomRefs2",extraId
			 * +Integer.toString(i)+id1+" "+extraId+Integer.toString(i)+id2);
			 * b.setId
			 * (extraId+Integer.toString(i)+id1+" "+extraId+Integer.toString
			 * (i)+id2); b.setOrder(ru.getBonds().get(j).getOrder());
			 * repeatUnitWithCountExpression.addBond(b); }
			 */
			moleculesInRepeatUnit.add(repeatUnitWithCountExpression);
		}
		return moleculesInRepeatUnit;
	}

	/**
	 * . Builds a single dendrimer CMLMolecule from an ArrayList<CMLMolecule>
	 * 
	 * @param molecules
	 *            - ArrayList<CMLMolecule>, separate repeat units used to build
	 *            the dendrimer
	 */
	private void addAllAtomsAndBonds(final ArrayList<CMLMolecule> molecules) {
		dendrimer = new CMLMolecule();
		ArrayList<ArrayList<String>> ids4AllRgroups = PolymerisationUtils
				.getRgroupConnectors(molecules);
		for (int i = 0; i < molecules.size(); i++) {
			PolymerisationUtils.addAtoms(molecules, i, dendrimer);
			PolymerisationUtils.addBonds(molecules, i, dendrimer);
		}
		int counter = ids4AllRgroups.get(0).size();
		for (int j = 0; j < ids4AllRgroups.get(0).size(); j++) {
			CMLBond b = new CMLBond();
			b.setAttribute("atomRefs2", ids4AllRgroups.get(0).get(j) + " "
					+ ids4AllRgroups.get(counter).get(0));
			b.setId(ids4AllRgroups.get(0).get(j) + " "
					+ ids4AllRgroups.get(counter).get(0));
			b.setAttribute("atomRefs2", ids4AllRgroups.get(0).get(j) + " "
					+ ids4AllRgroups.get(counter).get(0));
			b.setId(ids4AllRgroups.get(0).get(j) + " "
					+ ids4AllRgroups.get(counter).get(0));
			b.setOrder("1");
			dendrimer.addBond(b);
			counter--;
		}
		// Eliminate R-R bonds in the first layer
		List<CMLBond> allBonds = dendrimer.getBonds();
		ArrayList<CMLBond> bothRAtoms = new ArrayList<CMLBond>();
		for (int v = 0; v < allBonds.size(); v++) {
			if (allBonds.get(v).getAtom(0).getElementType().equals("R")) {
				bothRAtoms.add(allBonds.get(v));
			}
		}
		for (int r = 0; r < bothRAtoms.size(); r++) {
			RRBonds rrval = new RRBonds(bothRAtoms.get(r), dendrimer);
			rrval.automaticallySetX1X2();
			rrval.createX1X2Bond();
			rrval.deleteRgroupsAndBonds();
			dendrimer = rrval.getUpdatedMolecule();
		}
		if (this.layers > 1) {
			recursivelyAddLayers(ids4AllRgroups);
		}

	}

	/**
	 * Takes the prebuilt dendrimer molecule and adds bonds between R-Elements
	 * using a pregenerated list of atoms with R-Element ids.
	 * 
	 * @param ids4AllRgroups
	 *            - ArrayList<ArrayList<String>>, ids of all the R groups in all
	 *            the CMLMolecules making up the dendrimer
	 */
	private void recursivelyAddLayers(
			final ArrayList<ArrayList<String>> ids4AllRgroups) {
		int requiredLayers = 1;
		int higherLayers = 1;
		int rgroupsLeft = 1;
		while (rgroupsLeft < ids4AllRgroups.get(0).size()) {
			for (int i = 1 + ((requiredLayers - 1) * ids4AllRgroups.get(0)
					.size()); i < 1
					+ ((requiredLayers - 1) * (ids4AllRgroups.get(0).size()))
					+ ids4AllRgroups.get(0).size(); i++) {
				CMLBond b = new CMLBond();
				b.setAttribute("atomRefs2", ids4AllRgroups.get(i).get(
						rgroupsLeft)
						+ " "
						+ ids4AllRgroups.get(
								i + rgroupsLeft
										* (ids4AllRgroups.get(0).size()))
								.get(0));
				b.setId(ids4AllRgroups.get(i).get(rgroupsLeft)
						+ " "
						+ ids4AllRgroups.get(
								i + rgroupsLeft
										* (ids4AllRgroups.get(0).size()))
								.get(0));
				b.setOrder("1");
				dendrimer.addBond(b);
			}
			rgroupsLeft++;

			// Eliminate R-R bonds in the first layer
			List<CMLBond> allBondsL2 = dendrimer.getBonds();
			ArrayList<CMLBond> bothRAtoms2 = new ArrayList<CMLBond>();
			for (int v = 0; v < allBondsL2.size(); v++) {
				if (allBondsL2.get(v).getAtom(0).getElementType().equals("R")) {
					bothRAtoms2.add(allBondsL2.get(v));
				}
			}
			for (int r = 0; r < bothRAtoms2.size(); r++) {
				RRBonds rrval = new RRBonds(bothRAtoms2.get(r), dendrimer);
				rrval.automaticallySetX1X2();
				rrval.createX1X2Bond();
				rrval.deleteRgroupsAndBonds();
				dendrimer = rrval.getUpdatedMolecule();
			}

		}
		if (this.layers == LAYER_MAXIMUM) {
			int rgroupsLeft2 = 1;
			while (rgroupsLeft2 < ids4AllRgroups.get(0).size()) {
				for (int i = 1 + ids4AllRgroups.get(0).size(); i < 1
						+ ((ids4AllRgroups.get(0).size()) * Math.pow(2,
								higherLayers)) + ids4AllRgroups.get(0).size(); i++) {
					CMLBond b = new CMLBond();
					int valToSelect = (int) (i + ((ids4AllRgroups.get(0).size()) * Math
							.pow(2, rgroupsLeft2)));
					b.setAttribute("atomRefs2", ids4AllRgroups.get(i).get(
							rgroupsLeft2)
							+ " " + ids4AllRgroups.get(valToSelect).get(0));
					b.setId(ids4AllRgroups.get(i).get(rgroupsLeft2) + " "
							+ ids4AllRgroups.get(valToSelect).get(0));
					b.setOrder("1");
					dendrimer.addBond(b);
				}
				rgroupsLeft2++;
				eliminateRRBonds();
			}
		}
	}

	/**
	 * Removes all X-R-R-Y bonds left over after the dendrimer has been built
	 * and replaces the bond between neighbouring atoms X-Y.
	 */
	private void eliminateRRBonds() {
		// Eliminate R-R bonds in the first layer
		List<CMLBond> allBondsL2 = dendrimer.getBonds();
		ArrayList<CMLBond> bothRAtoms2 = new ArrayList<CMLBond>();
		for (int v = 0; v < allBondsL2.size(); v++) {
			if ("R".equals(allBondsL2.get(v).getAtom(0).getElementType())) {
				bothRAtoms2.add(allBondsL2.get(v));
			}
		}
		for (int r = 0; r < bothRAtoms2.size(); r++) {
			RRBonds rrval = new RRBonds(bothRAtoms2.get(r), dendrimer);
			rrval.automaticallySetX1X2();
			rrval.createX1X2Bond();
			rrval.deleteRgroupsAndBonds();
			dendrimer = rrval.getUpdatedMolecule();
		}
	}

	/**
	 * Generates 3D coordinates given a 2D XML representation of the built
	 * polymer.
	 * 
	 * @param pmlStringNoCoordinates
	 *            - a 2D XML representation of the built polymer
	 * @return XML string representation of the polymer with 3D coordinates.
	 *         Coordinate generation is done using an external dependencies,
	 *         from the Chemistry Development kit ModelBuilder3D class @link
	 *         (http
	 *         ://pele.farmbio.uu.se/nightly/api/org/openscience/cdk/modeling
	 *         /builder3d/ModelBuilder3D.html).
	 * @throws Exception
	 *             if 3D coordinate generation fails.
	 */
	public final String add3DCoordinates(final String pmlStringNoCoordinates)
			throws Exception {
		String molWithCoords = PolymerisationUtils
				.add3DCoordinates(pmlStringNoCoordinates);
		return molWithCoords;
	}

	/*
	 * public static void main(String[] args) throws Exception {
	 * Dendrimerisation dendrimerisation = new Dendrimerisation(3,
	 * "c1(C[R])cc(C[R])cc(C[R])c1"); CMLFragment frag =
	 * dendrimerisation.getFragment(); Document pmlBasicDoc =
	 * PolymerisationUtils.createPMLBasic(frag); Document pmlIntermediateDoc =
	 * PolymerisationUtils .createPMLIntermediate(frag);
	 * System.out.println(pmlBasicDoc.toXML());
	 * System.out.println(pmlIntermediateDoc.toXML()); }
	 */
	/*
	 * Document pmlBasicDoc = dendrimerisation.createPMLBasic(); Document
	 * pmlIntermediateDoc = dendrimerisation.createPMLIntermediate();
	 * System.out.println(pmlBasicDoc.toXML());
	 * System.out.println(pmlIntermediateDoc.toXML()); } //String result =
	 * //dendrimerisation.getPolymer().toXML(); System.out.println(result);
	 * //Document d = dendrimerisation.asXML(); System.out.println(d.toXML()); }
	 * //
	 */

	/**
	 * Returns CMLMolecule representation of the built polymer.
	 * 
	 * @return dendrimer (non-Javadoc)
	 * @see org.xmlcml.cml.tools.polymer.Polymerisation#getPolymer()
	 */
	public final CMLMolecule getPolymer() {
		//boolean buildable = PolymerisationUtils.isBuildable(getFragment());
		boolean buildable = true;
		ArrayList<CMLMolecule> separateMolecules;
		if (buildable == true) {
			separateMolecules = getSeparateComponents();
			addAllAtomsAndBonds(separateMolecules);
			return dendrimer;
		} else {

			UnbuildableException unbuildableException = new UnbuildableException(
					"Can't be built");
			unbuildableException.getMessage();
			return null;
		}
	}

	/**
	 * Method returns a fully atomistic CML Document representation of the
	 * polymer.
	 * 
	 * @return Document
	 */
	public final Document createCML() {
		Builder builder = new Builder();
		Document doc = null;
		try {
			doc = builder.build(dendrimer.toXML(), "");
		} catch (ValidityException e) {
			e.printStackTrace();
		} catch (ParsingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return doc;
	}

	/**
	 * Creates CMLFragment used in PML document generation.
	 * 
	 * @return CMLFragment
	 */
	public final CMLFragment getFragment() {
		CMLFragment parentFragment = new CMLFragment();
		ArrayList<CMLMolecule> separateMolecules = new ArrayList<CMLMolecule>();
		separateMolecules = getSeparateComponents();
		ArrayList<CMLAtom> centreMoleculeRgroups = PolymerisationUtils
				.getRElements(repeatUnitCentre);
		/*
		 * ArrayList<CMLAtom> centreMoleculeRgroups = PolymerisationUtils
		 * .getRElements(repeatUnitCentre); int numberOfJoins; if (layers == 0)
		 * { numberOfJoins = 0; separateMolecules.add(repeatUnitCentre); } else
		 * if (layers < 0) {
		 * System.out.println("Error, can't have negative layers"); } else if
		 * (layers > 0) { int rgroupsPerRepeatUnit =
		 * centreMoleculeRgroups.size(); numberOfJoins = (int)
		 * (rgroupsPerRepeatUnit (Math.pow(2, layers)) - 2); separateMolecules =
		 * countExpressionExpander(numberOfJoins, repeatUnitCentre, "den"); }
		 */
		ArrayList<CMLFragment> blankRepeatUnitFragments = new ArrayList<CMLFragment>();
		ArrayList<CMLMolecule> blankCMLMolecules = new ArrayList<CMLMolecule>();
		for (int i = 0; i < separateMolecules.size(); i++) {
			CMLMolecule m = new CMLMolecule();
			m.setRef("RU" + Integer.toString(i));
			blankCMLMolecules.add(m);
			CMLFragment f = new CMLFragment();
			f.addMolecule(m);
			blankRepeatUnitFragments.add(f);
		}
		ArrayList<ArrayList<String>> ids4AllRgroups = PolymerisationUtils
				.getRgroupConnectors(separateMolecules);
		// Build dendrimer add joins
		String moleculeRefs = "PREVIOUS NEXT";
		// Join 0 -> 1..R
		for (int i = 0; i < centreMoleculeRgroups.size(); i++) {
			CMLJoin centreTo1stLayer = new CMLJoin();
			centreTo1stLayer.setAtomRefs2(ids4AllRgroups.get(0).get(i) + " "
					+ ids4AllRgroups.get(i + 1).get(0));
			centreTo1stLayer.processMoleculeRefs2AndAtomRefs2(separateMolecules
					.get(0), separateMolecules.get(i + 1));
			centreTo1stLayer.setMoleculeRefs2(moleculeRefs);
			if (i == 0) {
				parentFragment.appendChild(blankRepeatUnitFragments.get(0));
				parentFragment.addJoin(centreTo1stLayer);
				parentFragment.appendChild(blankRepeatUnitFragments.get(i + 1));
			} else {
				parentFragment.addJoin(centreTo1stLayer);
				parentFragment.appendChild(blankRepeatUnitFragments.get(i + 1));
			}
		}
		// Need to join for higher layers
		int requiredLayers = 1;
		int higherLayers = 1;
		int rgroupsLeft = 1;
		while (rgroupsLeft < ids4AllRgroups.get(0).size()) {
			for (int i = 1 + ((requiredLayers - 1) * ids4AllRgroups.get(0)
					.size()); i < 1
					+ ((requiredLayers - 1) * (ids4AllRgroups.get(0).size()))
					+ ids4AllRgroups.get(0).size(); i++) {
				CMLJoin firstTo2ndLayer = new CMLJoin();
				firstTo2ndLayer.setAtomRefs2(ids4AllRgroups.get(i).get(
						rgroupsLeft)
						+ " "
						+ ids4AllRgroups.get(
								i + rgroupsLeft
										* (ids4AllRgroups.get(0).size()))
								.get(0));
				firstTo2ndLayer.processMoleculeRefs2AndAtomRefs2(
						separateMolecules.get(rgroupsLeft), separateMolecules
								.get(i + rgroupsLeft
										* (ids4AllRgroups.get(0).size())));
				firstTo2ndLayer.setMoleculeRefs2(moleculeRefs);
				parentFragment.addJoin(firstTo2ndLayer);
				parentFragment.appendChild(blankRepeatUnitFragments.get(i
						+ rgroupsLeft * (ids4AllRgroups.get(0).size())));
			}
			rgroupsLeft++;
		}
		// Add third layer
		if (this.layers == LAYER_MAXIMUM) {
			int rgroupsLeft2 = 1;
			while (rgroupsLeft2 < ids4AllRgroups.get(0).size()) {
				for (int i = 1 + ids4AllRgroups.get(0).size(); i < 1
						+ ((ids4AllRgroups.get(0).size()) * Math.pow(2,
								higherLayers)) + ids4AllRgroups.get(0).size(); i++) {
					CMLJoin secondToThirdLayer = new CMLJoin();
					int valToSelect = (int) (i + ((ids4AllRgroups.get(0).size()) * Math
							.pow(2, rgroupsLeft2)));
					secondToThirdLayer.setAtomRefs2(ids4AllRgroups.get(i).get(
							rgroupsLeft2)
							+ " " + ids4AllRgroups.get(valToSelect).get(0));
					secondToThirdLayer.processMoleculeRefs2AndAtomRefs2(
							separateMolecules.get(i), separateMolecules
									.get(valToSelect));
					secondToThirdLayer.setMoleculeRefs2(moleculeRefs);
					parentFragment.addJoin(secondToThirdLayer);
					parentFragment.appendChild(blankRepeatUnitFragments
							.get(valToSelect));
				}
				rgroupsLeft2++;
			}
		}
		return parentFragment;
	}

	/**
	 * Method to return separate unjoined repeat units making up the dendrimer.
	 * 
	 * @return ArrayList<CMLMolecule>
	 */
	private ArrayList<CMLMolecule> getSeparateComponents() {
		ArrayList<CMLMolecule> separateMolecules = new ArrayList<CMLMolecule>();
		ArrayList<CMLAtom> centreMoleculeRgroups = PolymerisationUtils
				.getRElements(repeatUnitCentre);
		int numberOfJoins;
		if (layers == 0) {
			numberOfJoins = 0;
			separateMolecules.add(repeatUnitCentre);
		} else if (layers < 0) {
			System.out.println("Error, can't have negative layers");
		} else if (layers > 0) {
			int rgroupsPerRepeatUnit = centreMoleculeRgroups.size();
			numberOfJoins = (int) (rgroupsPerRepeatUnit * (Math.pow(2, layers)) - 2);
			separateMolecules = countExpressionExpander(numberOfJoins,
					repeatUnitCentre, "den");

			// return separateMolecules;
		}
		return separateMolecules;
	}
}
